package org.dicr.radius.dict;

import java.io.*;
import java.lang.ref.*;
import java.net.*;
import java.text.*;
import java.util.*;

import org.dicr.radius.*;
import org.slf4j.*;

/**
 * Abstract Dictionary. Base class implementation.<br/>
 * Each vendor data cached in memory with {@link SoftReference}.<br/>
 *
 * @author <a href="http://dicr.org/">Igor (Dicr) Tarasov</a> {@literal <develop@dicr.org>}
 * @version 141026
 */
public abstract class AbstractDictionary implements RadiusDictionary {

	/** maximal value of attribute code */
	public static final int CODE_MAX = 0x0FF;

	/** vendor code for standard attribute */
	public static final int VENDOR_NONE = 0;

	/** maximal value for vendor code */
	public static final int VENDOR_MAX = 0x0FFFFFF;

	/** Logger */
	private static final Logger log = LoggerFactory.getLogger(AbstractDictionary.class);

	/** Singleton instance */
	protected static final Map<URL, Reference<AbstractDictionary>> INSTANCES = new HashMap<>();

	/**
	 * Vendors map implementation.<br/>
	 * Each code belong to single vendor.
	 * 
	 * @see <a href="http://www.iana.org/assignments/enterprise-numbers/enterprise-numbers">IANA Enterprise Numbers</a>
	 */
	protected Map<Integer, String> vendors = new HashMap<>();

	/**
	 * Radius attributes descriptors map implementation.<br/>
	 * Each attribute code may have multiple name aliases, but attribute names is unique in all vendors.
	 */
	private Reference<Map<String, AttDescriptor>> attributes = null;

	/** Data resource URL */
	protected URL url = null;

	/**
	 * Constructor
	 * 
	 * @param url resource url
	 */
	protected AbstractDictionary(final URL url) {
		if (url == null) throw new IllegalArgumentException("null url");
		this.url = url;
		AbstractDictionary.log.info("created dictionary for URL: " + url);
	}

	/**
	 * Force load data from resource url
	 * 
	 * @return loaded data from resource
	 * @throws IOException resource read error
	 * @throws ParseException error in resource
	 */
	public abstract Map<Integer, VendorDescriptor> loadData() throws IOException, ParseException;
	
	/**
	 * Check if data is loaded and cached.<br/>
	 * SoftReference data cache in memory cause data may be discarded
	 * 
	 * @return true if data cached in memory
	 */
	public synchronized boolean isDataLoaded() {
		return this.attributes != null && this.attributes.get() != null;
	}
	
	/**
	 * Import data from other dictionary
	 * 
	 * @param dict dictionary to import data
	 */
	public synchronized void importData(AbstractDictionary dict) {
		if (dict == null) throw new IllegalArgumentException("null dictionary");
		this.attributes = new SoftReference<>(dict.getAttributes());
		this.vendors = new HashMap<>(dict.vendors);
		AbstractDictionary.log.debug("imported " + this.vendors.size() + " vendors, " + this.attributes.get().size() + " attributes");
	}

	/**
	 * Export formatted data to output stream
	 * 
	 * @param out output to save
	 * @throws IOException writing error
	 */
	public abstract void exportData(OutputStream out) throws IOException;

	/**
	 * Return attributes data
	 * 
	 * @return attributes
	 */
	protected final synchronized Map<String, AttDescriptor> getAttributes() {
		Map<String, AttDescriptor> data = this.attributes != null ? this.attributes.get() : null;
		if (data == null) try {
			this.vendors.clear();
			this.attributes = null;
			Map<Integer, VendorDescriptor> parsed = this.loadData();
			data = new HashMap<>();
			for (final Integer code : parsed.keySet()) {
				VendorDescriptor vendor = parsed.get(code);
				this.vendors.put(code, vendor.name);
				for (AttDescriptor desc : vendor.attributes.values())
					data.put(desc.getName(), desc);
			}
			this.attributes = new SoftReference<>(data);
			AbstractDictionary.log.debug("loaded " + this.vendors.size() + " vendors, " + data.size() + " attributes");
		} catch (final Exception ex) {
			this.vendors.clear();
			this.attributes = null;
			data = null;
			AbstractDictionary.log.warn("error loading radius dictionary: " + this.url, ex);
		}
		return data != null ? data : Collections.emptyMap();
	}

	/**
	 * Create map of data for export form
	 * 
	 * @return map of internal data
	 */
	protected synchronized Map<Integer, VendorDescriptor> buildExportData() {
		Map<Integer, VendorDescriptor> data = null;
		if (!this.isDataLoaded()) this.getAttributes();
		data = new HashMap<>();
		for (Integer vcode : this.vendors.keySet())
			data.put(vcode, new VendorDescriptor(this.vendors.get(vcode)));
		Map<String, AttDescriptor> attribs = this.getAttributes();
		for (AttDescriptor attr : attribs.values()) {
			VendorDescriptor vendor = data.get(Integer.valueOf(attr.getVendor()));
			if (vendor == null) {
				AbstractDictionary.log.warn("internal data error: no vendor code=" + attr.getVendor() + " in vendors cache");
				continue;
			}
			vendor.attributes.put(attr.getName(), attr);
		}
		return data;
	}

	/**
	 * Vendor Descriptor
	 */
	protected static class VendorDescriptor {

		/** vendor name */
		protected String name;

		/** attributes */
		protected Map<String, AttDescriptor> attributes = new HashMap<>();

		/**
		 * @param aName name of vendor
		 */
		protected VendorDescriptor(final String aName) {
			if (aName == null || aName.trim().isEmpty()) throw new IllegalArgumentException("null name");
			this.name = aName;
		}
	}

	/**
	 * Implementation of {@link org.dicr.radius.RadiusDictionary.AttributeDescriptor AttributeDescriptor}
	 */
	protected static class AttDescriptor implements AttributeDescriptor {

		private static final long serialVersionUID = 2920644838097822988L;
		private final int vendor;
		private final int code;
		private final String name;
		private final ValueType type;
		private final String encoding;
		private Map<String, Long> values = null;

		/**
		 * Constructor
		 *
		 * @param vendor vendor code
		 * @param code attribute code
		 * @param name attribute name
		 * @param type attribute type
		 * @param encoding value encoding
		 */
		public AttDescriptor(final int vendor, final int code, final String name, final ValueType type, final String encoding) {
			if (vendor < 0 || vendor > AbstractDictionary.VENDOR_MAX) throw new IllegalArgumentException("vendor=" + vendor);
			this.vendor = vendor;
			if (code < 0 || (vendor == AbstractDictionary.VENDOR_NONE && code > AbstractDictionary.CODE_MAX)) throw new IllegalArgumentException(
					"code=" + code);
			this.code = code;
			if (name == null || name.trim().isEmpty()) throw new IllegalArgumentException("name=" + name);
			this.name = name;
			if (type == null) throw new IllegalArgumentException("null type");
			this.type = type;
			this.encoding = encoding;
		}

		/**
		 * Add new named attribute value
		 *
		 * @param aName name of attribute value
		 * @param value data value of named attribute value
		 */
		protected void addValue(final String aName, final long value) {
			if (aName == null || aName.trim().isEmpty()) throw new IllegalArgumentException("empty name");
			if ((value & 0x0FFFFFFFF) != value) throw new IllegalArgumentException("value=" + value);
			if (this.values == null) this.values = new HashMap<>();
			this.values.put(aName, Long.valueOf(value));
		}

		@Override
		public int getVendor() {
			return this.vendor;
		}

		@Override
		public int getCode() {
			return this.code;
		}

		@Override
		public String getName() {
			return this.name;
		}

		@Override
		public ValueType getType() {
			return this.type;
		}

		@Override
		public String getEncoding() {
			return this.encoding;
		}

		@Override
		public Map<String, Long> listValues() {
			return this.values != null ? Collections.unmodifiableMap(this.values) : Collections.emptyMap();
		}

		@Override
		public final AttDescriptor clone() {
			final AttDescriptor desc = new AttDescriptor(this.vendor, this.code, this.name, this.type, this.encoding);
			desc.values = this.values != null ? new HashMap<>(this.values) : null;
			return desc;
		}

		@Override
		public final int compareTo(final AttributeDescriptor desc) {
			if (desc == null) throw new IllegalArgumentException("can't compare to null object");
			if (desc == this) return 0;
			if (this.vendor < desc.getVendor()) return -1;
			else if (this.vendor == desc.getVendor()) {
				if (this.code < desc.getCode()) return -1;
				else if (this.code == desc.getCode()) return this.name.compareTo(desc.getName());
				return 1;
			}
			return 1;
		}

		@Override
		public final int hashCode() {
			int result = 16;
			result = result * 31 + this.vendor;
			result = result * 31 + this.code;
			result = result * 31 + this.name.hashCode();
			return result;
		}

		@Override
		public final boolean equals(final Object obj) {
			if (obj == null) return false;
			if (obj == this) return true;
			if (this.getClass() != obj.getClass()) return false;
			final AttDescriptor other = (AttDescriptor) obj;
			return this.vendor == other.vendor && this.code == other.code && this.name.equalsIgnoreCase(other.name);
		}

		@Override
		public final String toString() {
			final StringBuilder sb = new StringBuilder("AttributeDescriptor[");
			if (this.vendor > 0) sb.append(this.vendor).append(",");
			sb.append(this.code).append("]");
			return sb.toString();
		}
	}

	@Override
	public synchronized String searchVendor(final int code) {
		if (code < 0 || code > AbstractDictionary.VENDOR_MAX) throw new IllegalArgumentException("code=" + code);
		if (this.vendors.isEmpty()) this.getAttributes();
		return this.vendors.get(Integer.valueOf(code));
	}

	@Override
	public synchronized Integer searchVendor(String name) {
		if (name == null) throw new IllegalArgumentException("null name");
		if (this.vendors.isEmpty()) this.getAttributes();
		Integer code = null;
		for (Integer vcode : this.vendors.keySet()) {
			if (this.vendors.get(vcode).equalsIgnoreCase(name)) {
				code = vcode;
				break;
			}
		}
		return code;
	}

	@Override
	public synchronized Collection<String> listVendors() {
		if (this.vendors.isEmpty()) this.getAttributes();
		return this.vendors.values();
	}

	@Override
	public AttDescriptor searchAttribute(final int vendor, final int code) {
		if (vendor < 0 || vendor > AbstractDictionary.VENDOR_MAX) throw new IllegalArgumentException("vendor=" + vendor);
		if (code < 0 || (vendor == AbstractDictionary.VENDOR_NONE && code > AbstractDictionary.CODE_MAX)) throw new IllegalArgumentException(
				"code=" + code);
		AttDescriptor desc = null;
		Map<String, AttDescriptor> data = this.getAttributes();
		for (AttDescriptor att : data.values()) {
			if (att.getVendor() == vendor && att.getCode() == code) {
				desc = att;
				break;
			}
		}
		return desc;
	}

	@Override
	public AttDescriptor searchAttribute(final String name) {
		if (name == null) throw new IllegalArgumentException("null name");
		return this.getAttributes().get(name);
	}
}
